package afcp.alumni.dao.base;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
import javax.jdo.Extent;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import net.sf.jsr107cache.Cache;
import net.sf.jsr107cache.CacheException;
import net.sf.jsr107cache.CacheFactory;
import net.sf.jsr107cache.CacheManager;
import net.sf.jsr107cache.CacheStatistics;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reflections.Reflections;
import org.springframework.stereotype.Component;

import afcp.alumni.action.base.MyActionBeanContext;
import afcp.alumni.model.SearchResult;
import afcp.alumni.model.base.DatastoreEntity;
import afcp.alumni.service.impl.LoginService;
import afcp.alumni.util.Conf;

import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entities;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.datanucleus.query.JDOCursorHelper;

// TODO use memcache.
// Idea is adding a set() to memcache after each put to datastore:
//	data.put()
//	memcache.set(user_id, data)
// Then at each load from datastore, start to check memcache:
//	data = memcache.get(user_id)
//	if data is None:
//	    data = Data.get_by_key_name(user_id)
//	    memcache.set(user_id, data)

// TODO store cursors in memcache - use a containing object for easier migration
// https://p.ota.to/blog/2013/4/pagination-with-cursors-in-the-app-engine-datastore/
// http://googlecloudplatform.blogspot.fr/2013/12/best-practices-for-app-engine-memcache.html
// Using the RAM with synchronized getter is also a solution I guess.

// TODO pour les membres, essayer de mettre @Id sur le String id au lieu de Key 
// maintenant qu'il n'y a plus d'entité parent.

// TODO se renseigner sur les Tasks (différées) pour l'upload d'un fichier de membres dans la datastore.

@Component
public abstract class DaoDatastoreJDO<T extends DatastoreEntity> extends DaoDatastoreCommon<T> {
	
	private static final Log logger = LogFactory.getLog(DaoDatastoreJDO.class);
	
	@Inject
	private PM pm;
	@Inject
	private LoginService loginService;
	@Inject
	private DaoCache cache;
	
	PersistenceManager getPm() {
		return pm.getPm();
	}
	void resetPm() {
		pm.resetPm();
	}



	@Override
	public List<T> findAll() {
		logger.info("Searching for entities " + this.className + "...");
		List<T> entities = new LinkedList<>();
		Extent<T> extent = getPm().getExtent(classObject, false);
		for (T entity : extent) {
		  entities.add(entity);
		}
		extent.closeAll();
		
		cache.putEntitiesById(entities, className);
		
		return entities;
	}
	
	List<Cursor> cursorsIndex = newCursorsIndex();
	private void resetCursorsIndex() {
		cursorsIndex = newCursorsIndex();
	}
	private List<Cursor> newCursorsIndex() {
		List<Cursor>cursorsIndex = new ArrayList<>();
		cursorsIndex.add(null); // first page, cursor is null
		return cursorsIndex;
	}

	// Synchronized because it's a cache of cursors shared between all the user
	// queries.
	private synchronized Cursor getCursor(int page, int perPage) {
		if (page > cursorsIndex.size()) {
			getMissingCursors(page, perPage);
		}
		return cursorsIndex.get(page-1);
	}
	private void setCursor(int page, Cursor cursor) {
		if (cursor != null) {
			if (page > cursorsIndex.size() + 1) {
				throw new IllegalStateException("Cannot set the cursor for "
						+ "page " + page + " with cursor index of size "
						+ cursorsIndex.size()
						+ ". max cursor page allowed for insertion is "
						+ (cursorsIndex.size() + 1) + " until the index grows.");
			} else if (page == cursorsIndex.size() + 1) {
//				for (int i=cursors.size()+1; i<page; i++) {
//					cursors.add(null);
//				}
				cursorsIndex.add(cursor);
			} else if (cursorsIndex.get(page-1) == null) {
				cursorsIndex.set(page-1, cursor);
			}
		}
	}
	
	private void getMissingCursors(int page, int perPage) {
		int s = cursorsIndex.size();
		Query q = getPm().newQuery(classObject);
		Cursor cursor = cursorsIndex.get(s - 1);
		Map<String, Object> extensionMap = null;
		extensionMap = new HashMap<String, Object>();
		q.setExtensions(extensionMap);
		q.setRange(0, perPage);
//		q.setResult(keyField.getName());
//		q.setFilter("mParentEncKey == parentKeyParam");
//        q.declareParameters("String parentKeyParam");
		for (int p = s + 1; p <= page; p++) {
			if (cursor != null) {
				extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor);
				q.setExtensions(extensionMap);
			}
			@SuppressWarnings("unchecked")
			List<T> executeResult = (List<T>) q.execute(/*getAncestorKey()*/);
//			if (executeResult.size() == perPage && p < page) {
//				throw new IllegalStateException("The requested page (" + page
//						+ ") is after the last cursor available for entity"
//						+ className + ". Something is wrong in code design...");
//			}
			cursor = JDOCursorHelper.getCursor(executeResult);
			setCursor(p, cursor);
		}
		
	}

//	public SearchResult<T> findOnePage(int page, int perPage) {
//		SearchResult<T> entities = new SearchResult<>(page, perPage);
//		entities.setTotalResults(this.getTotalResults());
//		if (entities.hasResults()) {
//			int firstIndexIncluded = (page-1)*perPage;
//			int lastIndexExcluded = page*perPage;
//			logger.info("Searching for entities " + this.className
//					+ " from element " + (firstIndexIncluded + 1)
//					+ " to element " + lastIndexExcluded + "...");
//			Query q = getPm().newQuery(classObject);
//			q.setRange(firstIndexIncluded, lastIndexExcluded);
//			@SuppressWarnings("unchecked")
//			List<T> executeResult = (List<T>) q.execute();
//			entities.getList().addAll(executeResult);
//			// This check avoid storing a useless cursor, ie when we are on the
//			// last page (less results than maximum results per page)
//			if (executeResult.size() == perPage) {
//				entities.setCursor(JDOCursorHelper.getCursor(executeResult));
//			}
//		}
//		return entities;
//	}
	private int pageToFirstIndexIncluded(int page, int perPage) {
		return (page-1)*perPage;
	}
	private int pageToLastIndexExcluded(int page, int perPage) {
		return page*perPage;
	}
	
	@Override
	public SearchResult<T> findOnePage(int page, int perPage) {
		SearchResult<T> entities = new SearchResult<>(page, perPage);
		entities.setTotalResults(this.getTotalResults());
		if (entities.hasResults()) {
			int firstIndexIncluded = pageToFirstIndexIncluded(page, perPage);
			int lastIndexExcluded = pageToLastIndexExcluded(page, perPage);
			Query q = getPm().newQuery(classObject);
//			Query q = getPm().newQuery("select from " + this.classObject.getName() +
//                    " where 'parent-pk' == keyParam " +
//                    "parameters com.google.appengine.api.datastore.Key keyParam");

			Cursor cursor = getCursor(page, perPage);
			if (cursor != null || page == 1) {
				logger.info("Searching for entities " + this.className
						+ " from element " + (firstIndexIncluded + 1)
						+ " to element " + lastIndexExcluded + " using cursor...");
				if (cursor != null) {
					Map<String, Object> extensionMap = new HashMap<String, Object>();
					extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, cursor);
					q.setExtensions(extensionMap);
				}
				q.setRange(0, perPage);
			} else {
				logger.info("Searching for entities " + this.className
				+ " from element " + (firstIndexIncluded + 1)
				+ " to element " + lastIndexExcluded + "...");
				q.setRange(firstIndexIncluded, lastIndexExcluded);
			}
			
//			q.setFilter("parent-pk == keyParam");
//			q.declareParameters("com.google.appengine.api.datastore.Key keyParam");
			
//			q.setFilter("mParentEncKey == parentKeyParam");
//	        q.declareParameters("String parentKeyParam");
			
			@SuppressWarnings("unchecked")
			List<T> executeResult = (List<T>) q.execute(/*getAncestorKey()*/);
			entities.getList().addAll(executeResult);
			// This check avoid storing a useless cursor, ie when we are on the
			// last page (less results than maximum results per page)
			if (executeResult.size() == perPage) {
				setCursor(page + 1, JDOCursorHelper.getCursor(executeResult));
//				entities.setCursor(JDOCursorHelper.getCursor(executeResult));
			}
		}
		return entities;
	}
	
//	public SearchResult<T> findWithCursor(int page, int perPage, Cursor previousCursor) {
//		SearchResult<T> entities = new SearchResult<>(page, perPage);
//		entities.setTotalResults(this.getTotalResults());
//		if (entities.hasResults()) {
//			int firstIndexIncluded = (page-1)*perPage;
//			int lastIndexExcluded = page*perPage;
//			logger.info("Searching for entities " + this.className
//					+ " from element " + (firstIndexIncluded + 1)
//					+ " to element " + lastIndexExcluded + " using cursor...");
//			Query q = getPm().newQuery(classObject);
//			q.setRange(0, perPage);
//			
//			if (previousCursor != null) {
//				Map<String, Object> extensionMap = new HashMap<String, Object>();
//				extensionMap.put(JDOCursorHelper.CURSOR_EXTENSION, previousCursor);
//				q.setExtensions(extensionMap);
//			}
//			
//			@SuppressWarnings("unchecked")
//			List<T> executeResult = (List<T>) q.execute();
//			entities.getList().addAll(executeResult);
//			// This check avoid storing a useless cursor, ie when we are on the
//			// last page (less results than maximum results per page)
//			if (executeResult.size() == perPage) {
//				entities.setCursor(JDOCursorHelper.getCursor(executeResult));
//			}
//		}
//		return entities;
//	}
	
	private long getTotalResults() {
		logger.info("Counting the total number of entities " + this.className + ".");
		Query q = getPm().newQuery(classObject);
//		q.setFilter("mParentEncKey == parentKeyParam");
//        q.declareParameters("String parentKeyParam");
		q.setResult("count(this)");
		long count;
		try {
			count = (long) q.execute(/*getAncestorKey()*/);
		} catch (Exception e) {
			logger.warn("Could not count entities, an error occurred...", e);
			count = -1;
		}
		return count;
	}
	
	@Override
	public T find(String id) {
		logger.info("Searching for entity " + this.className + " with id: " + id + "...");
		Key k = KeyFactory.createKey(this.getAncestorKey(), className, id);
		return find(k);
	}

	@Override
	public T findFromKeyString(String key) {
		Key k = KeyFactory.stringToKey(key);
		return find(k);
	}
	
	private T find(Key key) {
		try {
			return getPm().getObjectById(classObject, key);
		} catch (JDOObjectNotFoundException e) {
			logger.info("Entity " + className + " with id: " + KeyFactory.keyToString(key) + " not found.");
			return null;
		}
	}
	
	@Override
	public SearchResult<T> find(List<String> ids, int page, int perPage) {
		return find(ids, page, perPage, ids.size());
	}

	@Override
	public SearchResult<T> find(List<String> ids, int page, int perPage,
			long totalResults) {
		List<String> keyIds = new LinkedList<String>();
		for (String id : ids) {
			Key thisKey = KeyFactory.createKey(this.getAncestorKey(),
					className, id);
			String keyId = KeyFactory.keyToString(thisKey);
			keyIds.add(keyId);
		}
		return findFromKeyStrings(keyIds, page, perPage, totalResults);
	}

//	@SuppressWarnings("unchecked")
//	@Override
//	public List<T> findFromKeyString(List<WithUniqueField> entities) {
//		setKey(entities);
//		
//		List<String> keys = new LinkedList<>();
//		for (T entity : entities) {
//			keys.add(KeyFactory.keyToString(entity.getKey()));
//		}
//		
//		// return getPm().getObjectsById(keys);
//		Query q = getPm().newQuery(classObject,
//				"select from " + className + " where :keys.contains(key)");
//		return (List<T>) q.execute(keys);
//	}


//	/**
//	 * @param field
//	 *            : should be bound to the entity corresponding to this DAO. If
//	 *            not, returns <code>null</code>.
//	 */
//	@Override
////	@Transactional
//	public <U extends WithUniqueField> U find(UniqueField<U> field) {
//		
//		// TODO subclass instead of throwing exception.
//		
//		// A different generic type is used in order to avoid subclassing this Dao.
//		// There are already too many subclasses.
//		if (!classObject.equals(field.entityClass())) {
//			logger.error("field of class "
//					+ field.getClass() + " is bound to "
//					+ field.getEntity().getClass()
//					+ ", but it sould be bound to " + classObject);
//			return null;
//		}
//		logger.info("Searching for entity " + this.className + " with unique field: " + field.getId() + "...");
//		setKey(field);
//		Key k = field.getKey();
//		try {
//			// It's weird but field.getClass() returns Class<? extends UniqueField>
//			// instead of Class<? extends UniqueField<U>>, so there is a warning.
//			// field being an instance of UniqueField<U>, we know which object is returned.
//			@SuppressWarnings({ "unchecked" })
//			UniqueField<U> foundUniqueField = getPm().getObjectById(field.getClass(), k);
//			return foundUniqueField.getEntity();
//		} catch (JDOObjectNotFoundException e) {
//			logger.info("Entity " + className + " with id: " + field.getId()
//					+ " not found.");
//			return null;
//		}
//	}
//	
//	<U extends WithUniqueField> UniqueField<U> findUniqueField(
//			UniqueField<U> field) {
//		setKey(field);
//		Key k = field.getKey();
//		try {
//			// It's weird but field.getClass() returns Class<? extends UniqueField>
//			// instead of Class<? extends UniqueField<U>>, so there is a warning.
//			// field being an instance of UniqueField<U>, we know which object is returned.
//			@SuppressWarnings({ "unchecked" })
//			UniqueField<U> foundUniqueField = getPm().getObjectById(field.getClass(), k);
//			return foundUniqueField;
//		} catch (JDOObjectNotFoundException e) {
//			logger.info("Entity " + field.getClass().getSimpleName()
//					+ " with id: " + field.getId() + " not found.");
//			return null;
//		}
//	}
	
	@Override
	public SearchResult<T> findFromKeyStrings(List<String> ids, int page, int perPage) {
		return findFromKeyStrings(ids, page, perPage, ids.size());
	}
	
	@Override
	public SearchResult<T> findFromKeyStrings(List<String> ids, int page, int perPage, long totalResults) {
		logger.info("Searching for entities " + this.className + " with ids: " + ids + "...");
		SearchResult<T> entities = new SearchResult<>(page, perPage);
		entities.setTotalResults(totalResults);
		if (entities.hasResults()) {
			    Query query = getPm().newQuery(classObject);
			    query.setFilter(":p.contains(key)");
				int firstIndexIncluded = pageToFirstIndexIncluded(page, perPage);
				int lastIndexExcluded = pageToLastIndexExcluded(page, perPage);
				// Does not work because of the implementation of .contains() of Google App Engine
//				query.setRange(firstIndexIncluded, lastIndexExcluded);
				List<String> searchedIds;
				if (lastIndexExcluded > ids.size()) {
					searchedIds = ids;
				} else {
					searchedIds = ids.subList(firstIndexIncluded, lastIndexExcluded);
				}
				@SuppressWarnings("unchecked")
				List<T> results = (List<T>) query.execute(searchedIds);
				entities.setList(results);
				entities.setTotalResults(results.size());
//			}
		}
		return entities;
	}
	
	public boolean processAllMembers(MemberProcessor<T> memberProcessor) {
		
		boolean success = true;

		int perPage = Conf.BUFFER_SIZE;
		SearchResult<T> result;
		int p = 1;
		do {
			result = findOnePage(p++, perPage);
			if (!result.getList().isEmpty()) {
				success &= memberProcessor.process(result.getList());
			}
		} while (result.getHasNextPage());

		return success;
	}
	
	@Override
	public boolean exists(T entity) {
		if (entity == null) {
			return false;
		}
//		if (entity instanceof WithUniqueField && entity.getKey() == null) {
//			UniqueField<? extends WithUniqueField> field = ((WithUniqueField) entity).getUniqueField();
//			if (field != null) {
//				return (find(field) != null);
//			} else {
//				return false;
//			}
//		} else {

			setKey(entity);
			logger.info("checking if entity " + className + " with id: " + entity.getId() + " exists in data store...");
			Key key = entity.getKey();
			if (key == null) {
				return false;
			}
			String id = KeyFactory.keyToString(key);
			T entityFound;
			try {
				entityFound = getPm().getObjectById(classObject, id);
			} catch (JDOObjectNotFoundException e) {
				entityFound = null;
			}
			boolean exists = entityFound != null;
			
			
//			Query q = getPm().newQuery(classObject, "key == keyParam");
//			q.declareParameters(Key.class.getName() + " keyParam");
////			q.execute(entity.getKey());
//		    @SuppressWarnings("unchecked")
////			List<T> entities = (List<T>) q.execute(entity.getTheKey());
//			List<T> entities = (List<T>) q.execute(id);
//		    T entityFound = null;
//		    boolean exists = entities != null && !entities.isEmpty() && (entityFound = entities.get(0)) != null;
		    
		    if (exists) {
				logger.info("entity " + className + " with id: " + entityFound.getId() + " and key: " + entityFound.getKey() + " already exists.");
		    } else {
		    	logger.info("entity " + className + " with id: " + entity.getId() + " not found.");
		    }
			return exists;
			
//		}
	}
	@Override
	public boolean addOrUpdate(T entity, boolean indexes) {
		setKey(entity);
		if (entity == null) {
			logger.error("Can't add null entity " + className + ".");
			return false;
		}
		if (logger.isInfoEnabled()) {
			String id = entity.getId();
			if (id != null) {
				logger.info("Add entity " + className + " with id: " + entity.getId() + ".");
			} else {
				logger.info("Add entity " + className + " " + entity + ".");
			}
		}
		// TODO
		

//		if (entity instanceof WithUniqueField) {
//			UniqueField<? extends WithUniqueField> uniqueField = ((WithUniqueField) entity).getUniqueField();
//			getPm().makePersistent(uniqueField);
//		}
		
		
		getPm().makePersistent(entity);
		if (indexes) {
			addToIndex(entity);
		}
		resetCursorsIndex();
		return true;
	}
	@Override
	public boolean addOrUpdate(T entity) {
		return addOrUpdate(entity, true);
	}
	@Override
	public boolean updateNoReindexingNoValidation(T entity) {
		try {
			getPm().makePersistent(entity);
			return true;
		} catch (Exception e) {
			logger.error("Could not persist entity " + className + " with id: "
					+ (entity != null ? entity.getId() : null), e);
			return false;
		}
	}
	@Override
	public boolean addOrUpdate(List<T> entities) {
		return addOrUpdate(entities, false);
	}
	@Override
	public boolean addOrUpdate(List<T> entities, boolean detach) {
		setKey(entities);
		if (entities == null || entities.contains(null)) {
			logger.error("Can't add null entity " + className + ".");
			return false;
		}
		if (!entities.isEmpty()) {
			if (logger.isInfoEnabled()) {
				List<String> ids = new LinkedList<>();
				for (T entity : entities) {
					ids.add(entity.getId());
				}
				logger.info("Add " + ids.size() + " entities " + className + ".");
			}
			getPm().makePersistentAll(entities);
			addToIndex(entities);
			resetCursorsIndex();
			if (detach) {
				int i = 0;
				for (T entity : entities) {
					entities.set(i, getPm().detachCopy(entity));
					i++;
				}
			}
		}
		return true;
	}
	@Override
	public void remove(T entity) {
		setKey(entity);
//		if (entity instanceof WithUniqueField) {
//			UniqueField<? extends WithUniqueField> uniqueField = ((WithUniqueField) entity).getUniqueField();
//			UniqueField<? extends WithUniqueField> attachedUniqueField = findUniqueField(uniqueField);
//			getPm().deletePersistent(attachedUniqueField.getEntity());
//			getPm().deletePersistent(attachedUniqueField);
//		} else {
			T foundEntity = find(entity.getKey());
			getPm().deletePersistent(foundEntity);
//		}
		resetCursorsIndex();
	}
	private void removeAttached(T entity) {
//		if (entity instanceof WithUniqueField) {
//			UniqueField<? extends WithUniqueField> uniqueField = ((WithUniqueField) entity).getUniqueField();
//			getPm().deletePersistent(uniqueField);
//		}
		getPm().deletePersistent(entity);
		resetCursorsIndex();
	}
	@Override
	public T removeFromKeyString(String id) {
		removeFromIndex(id);
		logger.info("Remove entity " + this.className + " with id: " + id + "." + ".");
		Key k = KeyFactory.stringToKey(id);
		T entity = find(k);
		T detachedCopy = getPm().detachCopy(entity);
		removeAttached(entity);
//		getPm().deletePersistent(entity);
//		getPm().flush();
		return detachedCopy;
	}
	@Override
	public T remove(String id) {
		logger.info("Remove entity " + this.className + " with id: " + id + "." + ".");
		T entity = find(id);
		removeFromIndex(KeyFactory.keyToString(entity.getKey()));
		T detachedCopy = getPm().detachCopy(entity);
		removeAttached(entity);
//		getPm().deletePersistent(entity);
		return detachedCopy;
	}
	@Override
	public boolean remove(List<T> entities) {
		// Delete in index
		List<String> ids = new LinkedList<>();
		for (T entity : entities) {
			ids.add(KeyFactory.keyToString(entity.getKey()));
		}
		logger.info("Remove entities " + this.className + " with ids: " + entities + ".");
		removeFromIndex(ids);
		// Delete corresponding "UniqueField" if applicable (ex : Login)
//		if (WithUniqueField.class.isAssignableFrom(classObject)) {
//			List<UniqueField<? extends WithUniqueField>> uniqueFields = new LinkedList<>();
//			for (T entity : entities) {
//				uniqueFields.add(((WithUniqueField) entity).getUniqueField());
//			}
//			logger.info("Remove all entities " + uniqueFieldClassObject + ".");
////			Query q = getPm().newQuery(uniqueFieldClassObject);
////			q.deletePersistentAll(uniqueFields);
//			getPm().deletePersistentAll(uniqueFields);
//		}
		// Pas sûr que ça marche... il ne faut pas plutôt une liste d'entités ?
		getPm().deletePersistentAll(entities);
		resetCursorsIndex();
		return true;
	}
	@Override
	public boolean removeAll() {
		removeAllFromIndex();
		Query q;
		// If the object processed by this DAO is an instance/implementation of WithUniqueField:
		// May change test into "uniqueFieldClassObject != null"
//		if (WithUniqueField.class.isAssignableFrom(classObject)) {
//			logger.info("Remove all entities " + uniqueFieldClassObject + ".");
//			q = getPm().newQuery(uniqueFieldClassObject);
//			q.deletePersistentAll();
////			getPm().flush();
//		}
		
		logger.info("Remove all entities " + className + ".");
		q = getPm().newQuery(classObject);
		q.deletePersistentAll();
//		getPm().flush();
//		resetPm();
		resetCursorsIndex();
		
		return true;
	}
	@Override
	public boolean update(T entity) {
		if (entity == null) {
			logger.error("Can't add null entity " + className + ".");
			return false;
		}
		addToIndex(entity);
		Key key = entity.getKey();
		if (key == null) {
			logger.error("Can't update entity of type " + className
					+ " if the object has no key.");
			return false;
		}
		// TODO subclass instead of testing interface
//		if (entity instanceof WithUniqueField) {
//			UniqueField<? extends WithUniqueField> oldEntityUniqueField = ((WithUniqueField) find(key))
//					.getUniqueField();
//			Key oldUniqueFieldKey = oldEntityUniqueField.getKey();
//			Key newUniqueFieldKey = ((WithUniqueField) entity).getUniqueField().getKey();
//			if (!newUniqueFieldKey.equals(oldUniqueFieldKey)) {
//				getPm().deletePersistent(oldEntityUniqueField);
//			}
//		}
		
		logger.info("Update entity " + className + " with id: " + entity.getId() + ".");
		getPm().makePersistent(entity);
		return true;
	}
	
	// deleting all session objects from the datastore

	public void clearSessions() {
		DatastoreService datastore = DatastoreServiceFactory
				.getDatastoreService();
		com.google.appengine.api.datastore.Query query = new com.google.appengine.api.datastore.Query(
				"_ah_SESSION");
		PreparedQuery results = datastore.prepare(query);

		logger.info("Deleting "
				+ results.countEntities(FetchOptions.Builder.withDefaults()
						.limit(1000)) + " sessions from data store");
		// for (Entity session : results.asIterable()) {
		// datastore.delete(session.getKey());
		// }
		List<Key> keys = new LinkedList<Key>();
		for (Entity session : results.asIterable()) {
			keys.add(session.getKey());
		}
		datastore.delete(keys);
	}

	// clearing everything in the cache, because sessions are also kept in
	// memcache

	public void clearCache() throws CacheException {
		CacheFactory cacheFactory = CacheManager.getInstance()
				.getCacheFactory();
		Cache cache = cacheFactory.createCache(Collections.emptyMap());

		CacheStatistics stats = cache.getCacheStatistics();
		logger.info("Clearing " + stats.getObjectCount() + " objects in cache");

		cache.clear();
	}
	
	@Override
	public List<KindInfo> listKindsWithStats() {
		DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
		PreparedQuery global = datastore.prepare(new com.google.appengine.api.datastore.Query("__Stat_Kind__"));

		List<KindInfo> kinds = new LinkedList<Dao.KindInfo>();
		for( Entity globalStat : global.asIterable() )
		{
		    Long totalBytes = (Long) globalStat.getProperty("bytes");
		    Long totalEntities = (Long) globalStat.getProperty("count");
		    String kindName = (String) globalStat.getProperty("kind_name");
		    
		    kinds.add(new KindInfo(kindName, totalEntities, totalBytes));
		}
		return kinds;
	}
	
	@Override
	public List<KindInfo> listKinds() {

		com.google.appengine.api.datastore.Query query = new com.google.appengine.api.datastore.Query(
				Entities.KIND_METADATA_KIND);
		DatastoreService datastoreService = DatastoreServiceFactory
				.getDatastoreService();

		Iterable<Entity> entityIterable = datastoreService.prepare(query)
				.asIterable();

		List<KindInfo> kinds = new LinkedList<Dao.KindInfo>();
		for (Entity entity : entityIterable) {
//			logger.info("Entity kind: " + entity.getKey().getName());
			kinds.add(new KindInfo(entity.getKey().getName(), -1, -1));
		}
		return kinds;
	}

	private static final String datastoreEntitiesPackage = "afcp.alumni.model.db";
	
	@Override
	public boolean removeByKindName(String kindName) {
		
//		final DatastoreService dss = DatastoreServiceFactory
//				.getDatastoreService();
//		final long start = System.currentTimeMillis();
//		int deletedCount = 0;
//		boolean isFinished = false;
//
//		while (System.currentTimeMillis() - start < 16384) {
//
//			final com.google.appengine.api.datastore.Query query = new com.google.appengine.api.datastore.Query(
//					kindName);
//			query.setKeysOnly();
//			final ArrayList<Key> keys = new ArrayList<Key>();
//
//			for (final Entity entity : dss.prepare(query).asIterable(
//					FetchOptions.Builder.withLimit(128))) {
//				keys.add(entity.getKey());
//			}
//
//			keys.trimToSize();
//
//			if (keys.size() == 0) {
//				isFinished = true;
//				break;
//			}
//
//			while (System.currentTimeMillis() - start < 16384) {
//				try {
//					dss.delete(keys);
//					deletedCount += keys.size();
//					break;
//				} catch (Throwable ignore) {
//					continue;
//				}
//			}
//		}
//		
//		if (isFinished) {
//			logger.info("Deletion finished");
//		}
//		logger.info("Entities deleted: " + deletedCount);
		
		Reflections reflections = new Reflections(datastoreEntitiesPackage);

		Class<?> deletedClass = null;
		Set<Class<? extends DatastoreEntity>> allClasses = reflections
				.getSubTypesOf(DatastoreEntity.class);

		for (Class<?> clazz : allClasses) {
			if (clazz.getSimpleName().equals(kindName)) {
				deletedClass = clazz;
				break;
			}
		}
		
		if (deletedClass == null) {
			logger.error("Could not delete class with name " + kindName
					+ " because it was not found in the package "
					+ datastoreEntitiesPackage);
			return false;
		}

		try {
			sapi.clearIndexOfKindName(kindName);
			logger.info("Remove all entities " + kindName + ".");
			Query q = getPm().newQuery(deletedClass);
			q.deletePersistentAll();
			resetCursorsIndex();
		} catch (Exception e) {
			logger.error("Error while removing all entities " + kindName + ".", e);
			return false;
		}
		return true;
	}
	
	private static final int TIMEOUT = 16384;
//	@Override
	public boolean removeByKindNameLowLevel(String kindName) {
		
		try {
			final DatastoreService dss = DatastoreServiceFactory
					.getDatastoreService();
			final long start = System.currentTimeMillis();
			int deletedCount = 0;
			boolean isFinished = false;

			while (System.currentTimeMillis() - start < TIMEOUT) {

				final com.google.appengine.api.datastore.Query query = new com.google.appengine.api.datastore.Query(
						kindName);
				query.setKeysOnly();
				final ArrayList<Key> keys = new ArrayList<Key>();

				for (final Entity entity : dss.prepare(query).asIterable(
						FetchOptions.Builder.withLimit(128))) {
					keys.add(entity.getKey());
				}

				keys.trimToSize();

				if (keys.size() == 0) {
					isFinished = true;
					break;
				}

				while (System.currentTimeMillis() - start < TIMEOUT) {
					try {
						dss.delete(keys);
						deletedCount += keys.size();
						break;
					} catch (Throwable ignore) {
						continue;
					}
				}
			}
			
			if (isFinished) {
				logger.info("Deletion finished for entity " + kindName + ".");
			} else {
				logger.info("Deletion not finished but stopped (time out: "
						+ TIMEOUT + ")");
			}
			logger.info("Entities deleted: " + deletedCount);
		} catch (Exception e) {
			logger.error("Error while removing all entities " + kindName + ".", e);
			return false;
		}
		return true;
	}
	
	private static final String RESERVED_ENTITY_PREFIX = "__";
	public boolean clearAllDatastore(MyActionBeanContext context) {
		try {
			logger.info("Logout current user");
			loginService.logout(context);
		} catch (Exception e) {
			logger.error("Error while logging out", e);
		}
		try {
			logger.info("Clear search API index");
			clearIndex();
		} catch (Exception e) {
			logger.error("Error while clearing search API indexes", e);
		}
		try {
			logger.info("Clear cache");
			clearCache();
		} catch (Exception e) {
			logger.error("Error while clearing cache", e);
		}
		try {
			logger.info("Clear sessions");
			clearSessions();
		} catch (Exception e) {
			logger.error("Error while clearing session", e);
		}
		try {
			List<KindInfo> listKinds = listKinds();
			String name;
			for (KindInfo kind : listKinds) {
				name = kind.getKindName();
				if (name.length() < 2
						|| !name.startsWith(RESERVED_ENTITY_PREFIX)) {
					logger.info("Clear datastore kind " + name);
					removeByKindNameLowLevel(name);
				} else {
					logger.info("Skip datastore kind " + name
							+ " because it is a reserved entity kind.");
				}
			}
		} catch (Exception e) {
			logger.error("Error while clearing datastore entities", e);
		}
		try {
			logger.info("Reset persistence manager");
			resetPm();
		} catch (Exception e) {
			logger.error("Error while reseting the persistence manager", e);
		}
		return true;
	}


	
//	// TODO faire fonctionner les balises @Transactional.
//	// Ceci permettra de ne plus s'embêter avec le persistentManager et de
//	// refaire un get() sur sa factory à chaque fois (et ainsi éviter les
//	// problèmes d'objets supprimés dont on garde des traces, et qui pose
//	// problème).
//	@Override
//	public Transaction newTransactionObject() {
//		return getPm().currentTransaction();
//	}
//	@Override
//	public void beginTransaction(Transaction txn) {
//		txn.begin();
//	}
//	@Override
//	public void commitTransaction(Transaction txn) {
//		txn.commit();
//		resetCursorsIndex();
//	}
//	@Override
//	public void rollbackTransaction(Transaction txn) {
//		try {
//			if (txn.isActive()) {
//				txn.rollback();
//			}
//		} catch (Exception e) {
//			logger.warn("Can't rollback the transaction. Exception message: " + e.getMessage());
//		}
//	}

}
